package IC.SemanticChecks;

import IC.Constants;
import IC.AST.*;
import IC.SymbolTable.SymbolKind;
import IC.SymbolTable.Symbol;
import IC.SymbolTable.SymbolTable;
import IC.Types.ArrayType;
import IC.Types.MethodType;
import IC.Types.Type;
import IC.Types.TypeTable;

/**
 * a visitor class that is used to run scope checks.
 */
public class ScopeCheckingVisitor implements Visitor {

	private SymbolTable LastClassST;
	
	//C'tor
	public ScopeCheckingVisitor(){
			
	}
		
	public Object visit(Program program) {
		for (ICClass icClass : program.getClasses()) {
			LastClassST = icClass.getScope();
			icClass.accept(this);
		}
		return null;
	}

	public Object visit(ICClass icClass) {
		
		for (Method m : icClass.getMethods()) {
			
			Symbol BaseSymbol = icClass.getScope().getParent().lookup(m.getName());
			Symbol ChildSymbol = icClass.getScope().lookup(m.getName());	
			
			// Overriding:
			if (BaseSymbol != null) {
				if (!BaseSymbol.getKind().equals(ChildSymbol.getKind())) {			
					if (BaseSymbol.getKind() != SymbolKind.FIELD) 
						SemanticError.handle(SemanticErrorType.INVALID_OVERRIDDING, m.getLine());
					else 
						SemanticError.handle(SemanticErrorType.INVALID_SHADOWING, m.getLine());
				}
				else 
					if (!BaseSymbol.getType().equals(ChildSymbol.getType())) 
						SemanticError.handle(SemanticErrorType.INVALID_OVERLOADING, m.getLine());
			}
			m.accept(this);
		}
		
		for (Field f : icClass.getFields()) {
			if (icClass.hasSuperClass()) 
				if (icClass.getScope().getParent().lookup(f.getName()) != null) 
					SemanticError.handle(SemanticErrorType.INVALID_SHADOWING, f.getLine());
				
		}
		return null;
	}

	
	public Object visit(VirtualMethod method) {
		for (Statement s : method.getStatements())
			s.accept(this);
		return null;
	}

	
	public Object visit(StaticMethod method) {
		for (Statement s : method.getStatements())
			s.accept(this);
		return null;
	}
	
	
	public Object visit(Assignment assignment) {
		assignment.getVariable().accept(this);
		assignment.getAssignment().accept(this);
		return null;
	}

	
	public Object visit(CallStatement callStatement) {
		callStatement.getCall().accept(this);
		return null;
	}

	
	public Object visit(Return returnStatement) {
		if (returnStatement.hasValue())
			returnStatement.getValue().accept(this);
		return null;
	}

	
	public Object visit(If ifStatement) {
		ifStatement.getCondition().accept(this);
		ifStatement.getOperation().accept(this);
		if (ifStatement.hasElse())
			ifStatement.getElseOperation().accept(this);
		return null;
	}

	
	public Object visit(While whileStatement) {
		whileStatement.getCondition().accept(this);
		whileStatement.getOperation().accept(this);
		return null;
	}


	
	public Object visit(StatementsBlock statementsBlock) {
		for (Statement s : statementsBlock.getStatements()) 
			s.accept(this);
		return null;
	}

	
	public Object visit(LocalVariable localVariable) {
		if (localVariable.hasInitValue())
			localVariable.getInitValue().accept(this);
		return null;
	}

	
	public Object visit(VariableLocation location) {
        if (location.isExternal()) {
        	SymbolTable varLocation = (SymbolTable) location.getLocation().accept(this);
        	if (varLocation == null) 
        		SemanticError.handle(SemanticErrorType.INVALID_FIELD, location.getLine());
        	else 
        	{
        		Symbol fieldSymbol = varLocation.virtualLookup(location.getName());
				if (fieldSymbol == null) 
					SemanticError.handle(SemanticErrorType.INVALID_FIELD, location.getLine());
        		 else 
    				return TypeTable.getClass(ArrayType.trimArraySign(fieldSymbol.getType().toString()));
        		
			}
        } 
        else 
        {
			Symbol locationType = location.getScope().lookup(location.getName(), location.getAppearanceOrder());
			if (locationType == null) 
				SemanticError.handle(SemanticErrorType.INVALID_VARIABLE_USAGE, location.getLine());
			else 
				return TypeTable.getClass(ArrayType.trimArraySign(locationType.getType().toString()));
        }
		return null;
	}

	
	public Object visit(ArrayLocation location) {
		location.getIndex().accept(this);
		return location.getArray().accept(this);
	}

	
	public Object visit(StaticCall call) {
		Symbol staticMethod = null;
		SymbolTable classSymbolTable = TypeTable.getClass(call.getClassName());
		if (classSymbolTable == null)
			SemanticError.handle(SemanticErrorType.INVALID_STATIC_METHOD, call.getLine());
	    else 
	    {	 
			staticMethod = classSymbolTable.staticLookup(call.getName());
			if (staticMethod == null) 
				SemanticError.handle(SemanticErrorType.INVALID_STATIC_METHOD, call.getLine());
		}
		
		for (Expression expr : call.getArguments()) 
			expr.accept(this);
	
		if (staticMethod != null && staticMethod.getType().isMethod()) {
			MethodType methodType = (MethodType)staticMethod.getType();
			Type returnType = methodType.getReturnType();
			return TypeTable.getClass(ArrayType.trimArraySign(returnType.toString()));
		}
		return null;
	}

	
	public Object visit(VirtualCall call) {
		SymbolTable callClass = null;
		
		if (call.isExternal()) 
			callClass = (SymbolTable) call.getLocation().accept(this);	
		else 	
			callClass = LastClassST;
		
		Symbol virtualMethod = null;
		if (callClass == null) 
			SemanticError.handle(SemanticErrorType.INVALID_VIRTUAL_METHOD, call.getLine());
	    else 
	    {
			virtualMethod = callClass.virtualLookup(call.getName());
			if (virtualMethod == null) 
				SemanticError.handle(SemanticErrorType.INVALID_VIRTUAL_METHOD, call.getLine());
		}
		
		for (Expression expr : call.getArguments()) 
			expr.accept(this);

		if (virtualMethod != null && virtualMethod.getType().isMethod()) {
			MethodType methodType = (MethodType)virtualMethod.getType();
			Type returnType = methodType.getReturnType();
			return TypeTable.getClass(ArrayType.trimArraySign(returnType.toString()));
		}
		return null;
	}

	
	public Object visit(This thisExpression) {
		if (thisExpression.getScope().virtualLookup(Constants.THIS) != null) 
			return LastClassST;
		SemanticError.handle(SemanticErrorType.INVALID_THIS, thisExpression.getLine());
		return null;
	}

	
	public Object visit(NewClass newClass) {
		return TypeTable.getClass(newClass.getName());
	}

	
	public Object visit(NewArray newArray) {
		newArray.getSize().accept(this);
		newArray.getType().accept(this);
		return null;
	}

	
	public Object visit(Length length) {
		length.getArray().accept(this);
		return null;
	}

	
	public Object visit(MathBinaryOp binaryOp) {
		binaryOp.getFirstOperand().accept(this);
		binaryOp.getSecondOperand().accept(this);
		return null;
	}

	
	public Object visit(LogicalBinaryOp binaryOp) {
		binaryOp.getFirstOperand().accept(this);
		binaryOp.getSecondOperand().accept(this);
		return null;
	}

	
	public Object visit(MathUnaryOp unaryOp) {
		unaryOp.getOperand().accept(this);
		return null;
	}

	
	public Object visit(LogicalUnaryOp unaryOp) {
		unaryOp.getOperand().accept(this);
		return null;
	}

	
	public Object visit(ExpressionBlock expressionBlock) {
		return expressionBlock.getExpression().accept(this);
	}
	
	
	public Object visit(Field field) {
		return null;
	}

	
	
	public Object visit(Literal literal) {
		return null;
	}

	
	public Object visit(LibraryMethod method) {
		return null;
	}

	
	public Object visit(Formal formal) {
		return null;
	}

	
	public Object visit(PrimitiveType type) {
		return null;
	}

	
	public Object visit(UserType type) {
		return null;
	}
	
	
	public Object visit(Break breakStatement) {
		return null;
	}

	
	public Object visit(Continue continueStatement) {
		return null;
	}

}
